package lolbatfsm.system;

import java.util.ArrayList;
import lolbatfsm.io.SimulationListener;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import lolbatfsm.io.SimulationController;

/**
 * Simulator is the singleton object that will actually
 * run the simulation.<br>
 * The simulation can be controlled by a <code>SimulationController</code>, and the
 * results of each iteration can be viewed by one or more <code>SimulationListener</code>s
 * (the common case is to have an user interface which implements both the
 * <code>SimulationController</code> and the <code>SimulationListener</code> interfaces).
 *
 * @author Matteo Battaglio <madbat87@gmail.com>
 */
public class Simulator implements Runnable {

    private static Simulator instance;

    private Collection<FSM> fsms;
    private Collection<EquivalenceClass> equivalenceClasses;
    private SimulationController controller;
    private final Collection<SimulationListener> listeners;
    private Thread thread;
    private int iterationDuration;
    private volatile boolean running = false;

    private Simulator() {
        listeners = new LinkedList<SimulationListener>();
    }

    /**
     * Returns the single Simulator instance (creating it at the first invocation
     * of this method).
     * @return The single Simulator intstance.
     */
    public static synchronized Simulator getInstance() {
        if (instance == null) {
            instance = new Simulator();
        }
        return instance;
    }
    
    public static void resetInstance() {
        instance = null;
    }

    /**
     * Sets the object which will control the simulation (e.g. select wchich transitions
     * to start among the enabled ones, etc).<br>
     * At the beginning of each iteration the Simulator - if necessary - will request
     * the simulation controller for the selection (eventually empty) of the transitions
     * to trigger.
     * @param controller the simulation controller.
     */
    public void setSimulationController(SimulationController controller) {
        this.controller = controller;
    }

    /**
     * Adds a listener for the simulation.<br>
     * The listeners will be notified of the results of each iteration and of the
     * eventual errors by the simulator.
     * @param listener a listener for the simulation
     * @return true if the listener has been added successfully, false otherwise
     */
    public boolean addSimulationListener(SimulationListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("listener parameter must be not null");
        }
        
        return listeners.add(listener);
    }

    /**
     * Starts the thread which will execute the simulation loop, using the provided
     * finite-state machines and equivalence classes.
     * @param finiteStateMachines
     * @param equivalenceClasses
     */
    public void startSimulation(Collection<FSM> finiteStateMachines,
            Collection<EquivalenceClass> equivalenceClasses) {
        if (running) {
            throw new SimulationException("Simulation already running");
        }
        if (controller == null) {
            throw new SimulationException("Simulation controller not set");
        }
        if (finiteStateMachines == null) {
            throw new IllegalArgumentException("finiteStateMachines parameter must be not null");
        }
        if (equivalenceClasses == null) {
            throw new IllegalArgumentException("equivalenceClasses parameter must be not null");
        }
        
        this.fsms = finiteStateMachines;
        this.equivalenceClasses = equivalenceClasses;
        running = true;
        thread = new Thread(this, "Simulator thread");
        thread.start();
        System.out.printf("Running? %b\n", running);
    }

    /**
     * Stops the simulation.
     */
    public void stopSimulation() {
        if (!running) {
            throw new SimulationException("Simulation already stopped");
        }
        for (FSM fsm : fsms) {
            fsm.reset();
        }
        running = false;
    }

    /**
     * Checks the preconditions for the iteration, then - if necessary - asks
     * the simulation controller to select a subset (eventually empty) of the
     * enabled transitions set and then executes the actual iteration or terminates the simulation,
     * depending on the state of the simulation (and on the simulation controller input).
     * @return true if the iteration completed successfully, false if an error occurred.
     */
    private boolean iterate() {
        //Se esiste almeno una transition con durata residua > 0 (ovvero in corso):
            //1a. L'utente PUO' scegliere almeno 1 transition fra quelle abilitate
            //delle FSM che non hanno transition in corso;
        //altrimenti:
            //1b. L'utente sceglie (OBBLIGATORIAMENTE) almeno 1 transition asioncrona o almeno
            //una coppia di transition rec. sincrone, fra le transition abilitate
            //(se ce ne sono);
        ArrayList<AsynchronousTransition> enabledAsyncTransitions = new ArrayList<AsynchronousTransition>();
        Map<EquivalenceClass, TreeSet<SynchronousTransition>> enabledSyncTransitionsMap = new TreeMap<EquivalenceClass, TreeSet<SynchronousTransition>>();
        for (EquivalenceClass ec : equivalenceClasses) {
            enabledSyncTransitionsMap.put(ec, new TreeSet<SynchronousTransition>());
        }

        boolean existPendingTransitions = false;
        boolean existEnabledTransitions = false;

        Collection<Transition> selectedTransitions = new TreeSet<Transition>();
        int minDuration = Integer.MAX_VALUE;

        //Per ogni FSM:
        for (FSM fsm : fsms) {
            if (fsm.hasPendingTransition()) {
                //esiste almeno una transition con durata residua > 0 (ovvero in corso)
                existPendingTransitions = true;
                Transition t = fsm.getPendingTransition();
                selectedTransitions.add(t);
                if (t.getResidualDuration() < minDuration) {
                    minDuration = t.getResidualDuration();
                }
            } else {
                if (fsm.hasEnabledTransitions()) {
                    //esiste almeno una transition abilitata a scattare
                    Collection<Transition> enabledFSMTransitions = fsm.getEnabledTransitions();
                    for (Transition transition : enabledFSMTransitions) {
                        if (transition instanceof AsynchronousTransition) {
                            enabledAsyncTransitions.add((AsynchronousTransition) transition);
                            existEnabledTransitions = true;
                        } else {
                            SynchronousTransition syncTransition = (SynchronousTransition) transition;
                            Set<SynchronousTransition> recSyncTransitions = enabledSyncTransitionsMap.get(syncTransition.getEquivalenceClass());
                            recSyncTransitions.add(syncTransition);
                            if (recSyncTransitions.size() >= 2) {
                                existEnabledTransitions = true;
                            }
                        }
                    }
                }
            }
        }

        boolean allowNullSelection = existPendingTransitions;
        ArrayList<TreeSet<SynchronousTransition>> enabledSyncTransitions = new ArrayList<TreeSet<SynchronousTransition>>(enabledSyncTransitionsMap.size());
        for (TreeSet<SynchronousTransition> recSyncTransitions : enabledSyncTransitionsMap.values()) {
            if (recSyncTransitions.size() >= 2) {
                enabledSyncTransitions.add(recSyncTransitions);
            }
        }

        int enabledTransitionsCount = enabledAsyncTransitions.size() + enabledSyncTransitions.size();
        Integer[][] selectedTransitionIndexes = new Integer[2][0];

        if (existEnabledTransitions) {
            boolean skipUserSelection = false;
            if (!existPendingTransitions && enabledTransitionsCount == 1) {
                skipUserSelection = true;
                if (!enabledAsyncTransitions.isEmpty()) {
                    Integer[] selectedAsyncTransitionIndexes = new Integer[1];
                    selectedAsyncTransitionIndexes[0] = 0;
                    selectedTransitionIndexes[0] = selectedAsyncTransitionIndexes;
                    selectedTransitionIndexes[1] = new Integer[0];
                } else {
                    Integer[] selectedSyncTransitionIndexes = new Integer[1];
                    selectedSyncTransitionIndexes[0] = 0;
                    selectedTransitionIndexes[0] = new Integer[0];
                    selectedTransitionIndexes[1] = selectedSyncTransitionIndexes;
                }
            }
            
            if (!skipUserSelection) {
                if (enabledTransitionsCount == 0) {
                    System.out.println("Errore");
                }
                //Richiedo all'interfaccia utente le transizioni selezionate fra quelle
                //abilitate, passando come parametro solo le fsm selezionabili, ovvero quelle che
                //non hanno una transition in corso e che hanno almeno una transition abilitata:
                selectedTransitionIndexes = controller.selectTransitions(enabledAsyncTransitions,
                                                                   enabledSyncTransitions,
                                                                   allowNullSelection);
            }
        } else {
            if (!existPendingTransitions) {
                //Simulazione giunta al termine
                stopSimulation();
                return false;
            }
        }

        if (selectedTransitionIndexes == null || 
                (selectedTransitionIndexes[0] == null && selectedTransitionIndexes[1] == null)) {
            notifyError("Null selection");
            return false;
        } else {
            int count = selectedTransitionIndexes[0].length + selectedTransitionIndexes[1].length;
            if (!allowNullSelection && count == 0) {
                notifyError("Null selection");
                return false;
            }
            if (selectedTransitionIndexes[0].length > 0) {
                for (int index : selectedTransitionIndexes[0]) {
                    AsynchronousTransition t = enabledAsyncTransitions.get(index);
                    selectedTransitions.add(t);
                    if (t.getResidualDuration() < minDuration) {
                        minDuration = t.getResidualDuration();
                    }
                }
            }
            if (selectedTransitionIndexes[1].length > 0) {
                for (int index : selectedTransitionIndexes[1]) {
                    TreeSet<SynchronousTransition> recSyncTransitions = enabledSyncTransitions.get(index);
                    selectedTransitions.addAll(recSyncTransitions);
                    Transition t = recSyncTransitions.first();
                    if (t.getResidualDuration() < minDuration) {
                        minDuration = t.getResidualDuration();
                    }
                }
            }

            try {
                //3. Esegue l'iterazione aggiornando le durate residue delle transition
                //scattate e aggiornando gli stati correnti delle FSM (dove necessario);
                executeIteration(selectedTransitions, minDuration);
            } catch (FSMException ex) {
                ex.printStackTrace();
                notifyError(ex.getMessage() + " -- Simulation aborted!");
                stopSimulation();
                return false;
            }
            return true;
        }
    }

    /**
     * Called by the <code>iterate()</code> method, it executes the iteration by
     * calling the <code>executeIteration()</code> method of each fsm that owns one
     * transition among the triggered ones.
     * @param triggeredTransitions the triggered transitions which will be updated
     * @param duration the duration of the transition
     * @throws FSMException
     */
    private void executeIteration(Collection<Transition> triggeredTransitions, int duration)
            throws FSMException {
        iterationDuration = duration;
        for (Transition transition : triggeredTransitions) {
            FSM fsm = transition.getOwnerFSM();
            fsm.executeIteration(transition, duration);
        }
    }

    /**
     * Notifies the listeners that an error occurred.
     * @param errorMessage the message containing infos about the error.
     */
    private void notifyError(String errorMessage) {
        for (SimulationListener listener : listeners) {
            listener.errorOccurred(errorMessage);
        }
    }

    /**
     * Notifies the listeners the result of the last iteration.
     */
    private void notifyIterationResults() {
        for (SimulationListener listener : listeners) {
            listener.iterationCompleted(iterationDuration, fsms);
        }
    }

    /**
     * Notifies the listeners that the simulation ended.
     */
    private void notifySimulationEnd() {
        for (SimulationListener listener : listeners) {
            listener.simulationEnded();
        }
    }

    /**
     * Contains the simulation loop.
     */
    public void run() {
        System.out.println("Simulator running...");
        while (running) {
            if (iterate()) {
                notifyIterationResults();
            }
        }
        
        notifySimulationEnd();
    }

}
